﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Build.BuildEngine;
using Microsoft.VisualStudio.Project;
using Microsoft.WowAddonStudio.Project;

using MsBuildProject = Microsoft.Build.BuildEngine.Project;

namespace Microsoft.WowAddonStudio.Project.Import
{
	/// <summary>
	/// Implements resource copies from source location to target location, and
	/// compiles an msbuild project.
	/// </summary>
	public class ProjectBuilder
	{
		#region Public events

		/// <summary>
		/// Event that is used to report progress after physical copies
		/// and project compilation.
		/// </summary>
		public event EventHandler ReportProgress;

		#endregion

		#region Private static members

		private const string outputPath = "OutputPath";
		private const string defaultTarget = "Deploy";

		private const string projectTargetFile = @"$(MSBuildExtensionsPath)\Microsoft\WowAddonStudio\2.0\WowAddonStudio.targets";

		private const string blizzardNamespace = "http://www.blizzard.com/wow/ui/";
		private const string propertyCondition = @" '$(Configuration)' == '' ";
		private const string propertyDefaultCondition = @" '$(Configuration)' == 'Default' ";
		private const string defaultProjectName = "NewProject";
		private const string projectExtension = ".wowproj";

		#endregion

		#region Private members

		private readonly Engine engine;

		#endregion

		#region Lifecycle methods

		/// <summary>
		/// Creates a new instance of <see cref="ProjectBuilder"/>.
		/// </summary>
		public ProjectBuilder()
		{
			engine = new Engine();
		}

		#endregion

		#region Public methods

		/// <summary>
		/// Moves addon items to target location, and creates an msbuild project with
		/// all addon items.
		/// </summary>
		/// <param name="targetPath">Path where addon items will be copied to.</param>
		/// <param name="addonItems">Addon items to copy and add to project.</param>
		/// <returns>An msbuild project containing items under appropriate nodes.</returns>
        public MsBuildProject Build(string targetPath, IEnumerable<IAddonItem> addonItems)
		{
			IEnumerable<IAddonItem> frameItems =
				addonItems.Where(addonItem => addonItem.Type == FileType.Frame);
			IEnumerable<IAddonItem> luaItems =
				addonItems.Where(addonItem => addonItem.Type == FileType.Code);
			IEnumerable<IAddonItem> tocItems =
				addonItems.Where(addonItem => addonItem.Type == FileType.TableOfContents);
			IEnumerable<IAddonItem> otherItems =
				addonItems.Where(addonItem => addonItem.Type == FileType.None);

			IEnumerable<string> relativeFolders = ExecutePhysicalCopies(targetPath, addonItems);
			Dictionary<IAddonItem, List<IAddonItem>> dependencies = CompileDependencies(frameItems, luaItems);

            MsBuildProject result = InitializeProject(tocItems);
			CreateFileItemGroup(frameItems, result);
			CreateFileItemGroup(luaItems, result, dependencies);
			CreateFileItemGroup(tocItems, result);
			CreateFileItemGroup(otherItems, result);
			CreateFolderItemGroup(result, relativeFolders, targetPath);

			//Save project...
			SaveProject(targetPath, tocItems, result);
			return result;
		}

		#endregion

		#region Private methods

		/// <summary>
		/// Fires the report progress.
		/// </summary>
		private void FireReportProgress()
		{
			if (ReportProgress != null)
				ReportProgress(this, new EventArgs());
		}

		/// <summary>
		/// Creates the folder item group.
		/// </summary>
		/// <param name="result">The result.</param>
		/// <param name="relativeFolders">The relative folders.</param>
		/// <param name="targetPath">The target path.</param>
        private static void CreateFolderItemGroup(MsBuildProject result, IEnumerable<string> relativeFolders,
		                                   string targetPath)
		{
			var relativeList = new List<string>(relativeFolders);

			if (relativeList.Count > 0)
			{
				BuildItemGroup workingItemGroup = result.AddNewItemGroup();

				foreach (var relativePath in relativeList)
				{
					string pathToBuildFile = relativePath;

					if (pathToBuildFile.LastIndexOf(Path.DirectorySeparatorChar) != pathToBuildFile.Length - 1)
						pathToBuildFile += Path.DirectorySeparatorChar;

					workingItemGroup.AddNewItem(ProjectFileConstants.Folder, pathToBuildFile);
				}
			}
		}

		/// <summary>
		/// Creates the file item group.
		/// </summary>
		/// <param name="addonItems">The addon items.</param>
		/// <param name="result">The result.</param>
		/// <param name="dependencies">The dependencies.</param>
        private static void CreateFileItemGroup(IEnumerable<IAddonItem> addonItems, MsBuildProject result,
		                                 IDictionary<IAddonItem, List<IAddonItem>> dependencies)
		{
			BuildItemGroup workingItemGroup = result.AddNewItemGroup();

			foreach (IAddonItem item in addonItems)
			{
				CreateBuildItem(workingItemGroup, item, dependencies);
			}
		}

		/// <summary>
		/// Creates the file item group.
		/// </summary>
		/// <param name="addonItems">The addon items.</param>
		/// <param name="result">The result.</param>
        private static void CreateFileItemGroup(IEnumerable<IAddonItem> addonItems, MsBuildProject result)
		{
			BuildItemGroup workingItemGroup = result.AddNewItemGroup();

			foreach (IAddonItem item in addonItems)
			{
				CreateBuildItem(workingItemGroup, item, null);
			}
		}

		/// <summary>
		/// Initializes the project.
		/// </summary>
		/// <param name="tocItems">The toc items.</param>
		/// <returns></returns>
        private MsBuildProject InitializeProject(IEnumerable<IAddonItem> tocItems)
		{
            var result = new MsBuildProject(engine) { DefaultTargets = defaultTarget };
			BuildPropertyGroup propertyGroup = result.AddNewPropertyGroup(false);
			BuildProperty property = propertyGroup.AddNewProperty(ProjectFileConstants.Configuration, "Default");
			property.Condition = propertyCondition;
			propertyGroup.AddNewProperty(ProjectFileConstants.Name, GetProjectName(tocItems));

			propertyGroup = result.AddNewPropertyGroup(false);
			propertyGroup.AddNewProperty(outputPath, ".");
			propertyGroup.Condition = propertyDefaultCondition;

			result.Imports.AddNewImport(projectTargetFile, null);
			return result;
		}

		/// <summary>
		/// Compiles the dependencies.
		/// </summary>
		/// <param name="frameItems">The frame items.</param>
		/// <param name="luaItems">The lua items.</param>
		/// <returns></returns>
		private static Dictionary<IAddonItem, List<IAddonItem>> CompileDependencies(
			IEnumerable<IAddonItem> frameItems, IEnumerable<IAddonItem> luaItems)
		{
			var dependencies = new Dictionary<IAddonItem, List<IAddonItem>>();

			foreach (var frameItem in frameItems)
			{
				try
				{
					using (Stream stream = frameItem.GetStream())
					{
						using (XmlReader reader = XmlReader.Create(stream))
						{
							// Read the content into an XNode
							XElement root = XElement.Load(reader);

							// Query the XNode for <Script /> elements
							XmlNameTable nameTable = reader.NameTable;
							var namespaceManager = new XmlNamespaceManager(nameTable);
							namespaceManager.AddNamespace("ans", blizzardNamespace);

							foreach (var scriptElement in root.XPathSelectElements("//ans:Script",
							                                                       namespaceManager))
							{
								// Get the filename of the reference script file
								XAttribute fileAttribute = scriptElement.Attribute("file");

								// Make sure we have a 'file' attriute
								if (fileAttribute != null)
								{
									//TODO: Refine comparison
									IAddonItem item = null;

									try
									{
										item =
											luaItems.First(luaItem => luaItem.RelativePath.EndsWith(fileAttribute.Value));
									}
									catch (InvalidOperationException)
									{
										//No lua item found
									}

									if (item != null)
									{
										if (!dependencies.ContainsKey(item))
											dependencies[item] = new List<IAddonItem>();

										dependencies[item].Add(frameItem);
									}
								}
							}
						}
					}
				}
				catch (XmlException)
				{
					// Could not read content as valid XML, skipping frame item
				}
			}

			return dependencies;
		}

		/// <summary>
		/// Executes the physical copies.
		/// </summary>
		/// <param name="targetPath">The target path.</param>
		/// <param name="addonItems">The addon items.</param>
		/// <returns></returns>
		private IEnumerable<string> ExecutePhysicalCopies(string targetPath,
		                                                  IEnumerable<IAddonItem> addonItems)
		{
			var relativeTargetFolders = new List<string>();

			foreach (IAddonItem item in addonItems)
			{
				AddonItemCopier.CopyAddonItem(targetPath, item);

				string directoryName = Path.GetDirectoryName(item.RelativePath);
				if (!string.IsNullOrEmpty(directoryName))
					relativeTargetFolders.Add(directoryName);

				FireReportProgress();
			}

			return relativeTargetFolders.Distinct();
		}

		/// <summary>
		/// Creates the build item.
		/// </summary>
		/// <param name="itemGroup">The item group.</param>
		/// <param name="item">The item.</param>
		/// <param name="dependencies">The dependencies.</param>
		private static void CreateBuildItem(BuildItemGroup itemGroup, IAddonItem item,
		                             IDictionary<IAddonItem, List<IAddonItem>> dependencies)
		{
			BuildItem buildItem = itemGroup.AddNewItem("Content", item.RelativePath);
			string metadataValue = null;

			switch (item.Type)
			{
				case FileType.Code:
					metadataValue = WowSubType.Code;
					break;
				case FileType.Frame:
					metadataValue = WowSubType.Frame;
					break;
				case FileType.TableOfContents:
					metadataValue = WowSubType.TableOfContents;
					break;
				case FileType.None:
				default:
					break;
			}

			if (null != metadataValue)
				buildItem.SetMetadata(ProjectFileConstants.SubType, metadataValue);

			if (dependencies != null && dependencies.ContainsKey(item))
			{
				foreach (var dependentUpon in dependencies[item])
				{
					buildItem.SetMetadata(ProjectFileConstants.DependentUpon, dependentUpon.RelativePath);
				}
			}
		}

		/// <summary>
		/// Saves the project.
		/// </summary>
		/// <param name="targetPath">The target path.</param>
		/// <param name="tocItems">The toc items.</param>
		/// <param name="result">The result.</param>
        private static void SaveProject(string targetPath, IEnumerable<IAddonItem> tocItems, MsBuildProject result)
		{
			string projectName = GetProjectName(tocItems);
			result.FullFileName = Path.Combine(targetPath, projectName + projectExtension);
			result.Save(result.FullFileName, Encoding.Unicode);
		}

		/// <summary>
		/// Gets the name of the project.
		/// </summary>
		/// <param name="tocItems">The toc items.</param>
		/// <returns></returns>
		private static string GetProjectName(IEnumerable<IAddonItem> tocItems)
		{
			string projectName;

			if (tocItems == null || tocItems.Count() == 0)
			{
				projectName = defaultProjectName;
			}
			else
			{
				IAddonItem firstTocItem = tocItems.First();
				projectName = Path.GetFileNameWithoutExtension(firstTocItem.RelativePath);
			}
			return projectName;
		}

		#endregion
	}
}